﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ColorhatApi.cs" company="Mar3ek">
//   http://mar3ek.wordpress.com
// </copyright>
// <summary>
//   Defines the ColorhatApi type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace ColorhatNet
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Text.RegularExpressions;
    using System.Threading.Tasks;

    using ColorhatNet.Exceptions;

    using JSON;

    /// <summary>
    /// The ColorHat API implementation.
    /// </summary>
    public class ColorhatApi
    {
        /// <summary>
        /// Stores the necessary HTTP cookies.
        /// </summary>
        private CookieContainer cookies;

        /// <summary>
        /// The snowflake ID.
        /// </summary>
        private string snowflakeId;

        /// <summary>
        /// The token hash.
        /// </summary>
        private string tokenHash;

        /// <summary>
        /// The token string.
        /// </summary>
        private string token;

        /// <summary>
        /// Stores the list of user's services.
        /// </summary>
        private ReadOnlyDictionary<uint, string> services = new ReadOnlyDictionary<uint, string>(new Dictionary<uint, string>());

        /// <summary>
        /// Stores the list of user's projects.
        /// </summary>
        private ReadOnlyDictionary<uint, string> projects = new ReadOnlyDictionary<uint, string>(new Dictionary<uint, string>());

        /// <summary>
        /// Stores the list of user's customers
        /// </summary>
        private ReadOnlyDictionary<uint, string> customers = new ReadOnlyDictionary<uint, string>(new Dictionary<uint, string>());

        /// <summary>
        /// Gets the user's services.
        /// </summary>
        public ReadOnlyDictionary<uint, string> Services
        {
            get
            {
                return this.services;
            }
        }

        /// <summary>
        /// Gets the user's projects.
        /// </summary>
        public ReadOnlyDictionary<uint, string> Projects
        {
            get
            {
                return this.projects;
            }
        }

        /// <summary>
        /// Gets the user's customers.
        /// </summary>
        public ReadOnlyDictionary<uint, string> Customers
        {
            get
            {
                return this.customers;
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is logged in.
        /// </summary>
        public bool IsLoggedIn
        {
            get
            {
                return cookies != null;
            }
        }

        /// <summary>
        /// Uploads a time segment record to the user's account.
        /// </summary>
        /// <param name="start">
        /// The start time of the segment.
        /// </param>
        /// <param name="end">
        /// The end time of the segment.
        /// </param>
        /// <param name="annotation">
        /// The annotation of the segment.
        /// </param>
        /// <param name="projectId">
        /// The project ID.
        /// </param>
        /// <param name="serviceId">
        /// The service ID.
        /// </param>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        /// <remarks>The time difference between start and end time should be at least 60 seconds.</remarks>
        public async Task<bool> UploadTimeSegment(DateTime start, DateTime end, string annotation, uint? projectId, uint? serviceId)
        {
            var requestJson = new JSONItem(
                    new KeyValuePair<string, JSONItem>(
                        "f",
                        new JSONItem("dm_time_Time::addTime")),
                    new KeyValuePair<string, JSONItem>(
                        "l",
                        new JSONItem("http://colorhat.com/apps/5/times")),
                    new KeyValuePair<string, JSONItem>(
                        "p",
                        new JSONItem(new[]
                                         {
                                             new JSONItem(
                                                 new KeyValuePair<string, JSONItem>("date", new JSONItem(start.ToString("yyyy-MM-dd"))),
                                                 new KeyValuePair<string, JSONItem>("humanTime", new JSONItem(string.Format("{0} to {1}", start.ToString("HH:mm"), end.ToString("HH:mm")))),
                                                 new KeyValuePair<string, JSONItem>("note", new JSONItem(annotation)),
                                                 new KeyValuePair<string, JSONItem>("projectId", new JSONItem(projectId.HasValue ? projectId.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null)),
                                                 new KeyValuePair<string, JSONItem>("serviceId", new JSONItem(serviceId.HasValue ? serviceId.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null))),
                                            new JSONItem("html")
                                         })),
                    new KeyValuePair<string, JSONItem>(
                        "t",
                        new JSONItem(
                            new JSONItem(this.token),
                            new JSONItem(this.tokenHash),
                            new JSONItem(this.snowflakeId))));

            return await this.SendApiRequest(requestJson);
        }

        /// <summary>
        /// Creates a new customer.
        /// </summary>
        /// <param name="customerName">
        /// The customer name.
        /// </param>
        /// <param name="listId">
        /// The list id.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        public async Task<bool> CreateCustomer(string customerName, uint? listId)
        {
            var requestJson = new JSONItem(
                    new KeyValuePair<string, JSONItem>(
                        "f",
                        new JSONItem("dm_customer_Customer::createCustomer")),
                    new KeyValuePair<string, JSONItem>(
                        "l",
                        new JSONItem("http://colorhat.com/apps/7/customers")),
                    new KeyValuePair<string, JSONItem>(
                        "p",
                        new JSONItem(new[]
                                         {
                                             new JSONItem(
                                                 new KeyValuePair<string, JSONItem>("listId", listId.HasValue ? new JSONItem(listId.Value) : new JSONItem()),
                                                 new KeyValuePair<string, JSONItem>("name", new JSONItem(customerName)))
                                         })),
                    new KeyValuePair<string, JSONItem>(
                        "t",
                        new JSONItem(
                            new JSONItem(this.token),
                            new JSONItem(this.tokenHash),
                            new JSONItem(this.snowflakeId))));

            var result = await this.SendApiRequest(requestJson);

            if (result)
            {
                await this.GetCustomers();
            }

            return result;
        }

        /// <summary>
        /// Deletes the specified customer.
        /// </summary>
        /// <param name="customerId">
        /// The Id of the customer to delete.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        public async Task<bool> DeleteCustomer(uint customerId)
        {
            const string TableName = "customers";
            return await this.DeleteColorhatObject(customerId, TableName);
        }

        /// <summary>
        /// Creates a new customer.
        /// </summary>
        /// <param name="customerId">
        /// The customer Id.
        /// </param>
        /// <param name="listId">
        /// The list Id.
        /// </param>
        /// <param name="projectName">
        /// The project Name.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        public async Task<bool> CreateProject(uint customerId, uint? listId, string projectName)
        {
            var requestJson = new JSONItem(
                    new KeyValuePair<string, JSONItem>(
                        "f",
                        new JSONItem("dm_project_Project::createProject")),
                    new KeyValuePair<string, JSONItem>(
                        "l",
                        new JSONItem("http://colorhat.com/apps/4/projects")),
                    new KeyValuePair<string, JSONItem>(
                        "p",
                        new JSONItem(new[]
                                         {
                                             new JSONItem(
                                                 new KeyValuePair<string, JSONItem>("customerId", new JSONItem(customerId)),
                                                 new KeyValuePair<string, JSONItem>("listId", listId.HasValue ? new JSONItem(listId.Value) : new JSONItem()),
                                                 new KeyValuePair<string, JSONItem>("name", new JSONItem(projectName)))
                                         })),
                    new KeyValuePair<string, JSONItem>(
                        "t",
                        new JSONItem(
                            new JSONItem(this.token),
                            new JSONItem(this.tokenHash),
                            new JSONItem(this.snowflakeId))));

            var result = await this.SendApiRequest(requestJson);

            if (result)
            {
                await this.GetProjects();
            }

            return result;
        }

        /// <summary>
        /// Deletes the specified project.
        /// </summary>
        /// <param name="projectId">
        /// The Id of the project to delete.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        public async Task<bool> DeleteProject(uint projectId)
        {
            const string TableName = "projects";
            return await this.DeleteColorhatObject(projectId, TableName);
        }

        /// <summary>
        /// Creates a new service.
        /// </summary>
        /// <param name="listId">
        /// The list Id.
        /// </param>
        /// <param name="serviceName">
        /// The service Name.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        public async Task<bool> CreateService(uint? listId, string serviceName)
        {
            var requestJson = new JSONItem(
                    new KeyValuePair<string, JSONItem>(
                        "f",
                        new JSONItem("dm_service_Service::createService")),
                    new KeyValuePair<string, JSONItem>(
                        "l",
                        new JSONItem("http://colorhat.com/apps/6/services")),
                    new KeyValuePair<string, JSONItem>(
                        "p",
                        new JSONItem(new[]
                                         {
                                             new JSONItem(
                                                 new KeyValuePair<string, JSONItem>("listId", listId.HasValue ? new JSONItem(listId.Value) : new JSONItem()),
                                                 new KeyValuePair<string, JSONItem>("name", new JSONItem(serviceName)))
                                         })),
                    new KeyValuePair<string, JSONItem>(
                        "t",
                        new JSONItem(
                            new JSONItem(this.token),
                            new JSONItem(this.tokenHash),
                            new JSONItem(this.snowflakeId))));

            var result = await this.SendApiRequest(requestJson);

            if (result)
            {
                await this.GetServices();
            }

            return result;
        }

        /// <summary>
        /// Deletes the specified service.
        /// </summary>
        /// <param name="serviceId">
        /// The Id of the service to delete.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        public async Task<bool> DeleteService(uint serviceId)
        {
            const string TableName = "services";
            return await this.DeleteColorhatObject(serviceId, TableName);
        }

        /// <summary>
        /// Attempts to log in and initialize this instance.
        /// </summary>
        /// <param name="userName">
        /// The username.
        /// </param>
        /// <param name="password">
        /// The password.
        /// </param>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        /// <returns>
        /// True if successful, otherwise false.
        /// </returns>
        public async Task<bool> LogOn(string userName, string password)
        {
            if (this.IsLoggedIn)
            {
                return true;
            }

            var formParts = new Dictionary<string, string> { { "username", userName }, { "password", password } };
                           
            var cookieContainer = new CookieContainer();

            try
            {
                using (var handler = new HttpClientHandler { CookieContainer = cookieContainer })
                using (var client = new HttpClient(handler))
                {
                    var getResult = await client.GetAsync("http://colorhat.com/login");
                    getResult.EnsureSuccessStatusCode();

                    var result = await client.PostAsync("http://colorhat.com/sendform/snowflake/login", new FormUrlEncodedContent(formParts));
                    result.EnsureSuccessStatusCode();

                    var receivedCookies = cookieContainer.GetCookies(new Uri("http://colorhat.com", UriKind.Absolute));
                    if (receivedCookies["sf_reg_u"] != null)
                    {
                        var snowflakeIdRegex = new Regex("Snowflake\\.sId\\s*=\\s*'(?<id>[a-fA-F0-9]+)'");
                        var tokenRegex = new Regex("Snowflake\\.token\\s*=\\s*'(?<token>[a-fA-F0-9]+)'");
                        var hashRegex = new Regex("Snowflake\\.tokenHash\\s*=\\s*'(?<hash>[a-fA-F0-9]+)'");

                        var content = await result.Content.ReadAsStringAsync();

                        this.snowflakeId = snowflakeIdRegex.Match(content).Groups["id"].Value;
                        this.token = tokenRegex.Match(content).Groups["token"].Value;
                        this.tokenHash = hashRegex.Match(content).Groups["hash"].Value;

                        this.cookies = cookieContainer;

                        await this.GetServices();
                        await this.GetProjects();
                        await this.GetCustomers();

                        return true;
                    }

                    return false;
                }
            }
            catch (Exception ex)
            {
                throw new ColorhatException("Exception occured while attempting to log on. See the inner exception for details.", ex);
            }
        }

        /// <summary>
        /// Refreshes the projects and services.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        public async Task Refresh()
        {
            if (!this.IsLoggedIn)
            {
                throw new InvalidOperationException("You must be logged in to perform this operation.");
            }

            await this.GetServices();
            await this.GetProjects();
        }

        /// <summary>
        /// Gets the user's services.
        /// </summary>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        private async Task GetServices()
        {
            this.services = await this.GetColorHatObjects(new Uri("http://colorhat.com/apps/6/services", UriKind.Absolute), 6, "service");
        }

        /// <summary>
        /// Gets the user's projects
        /// </summary>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        private async Task GetProjects()
        {
            this.projects = await this.GetColorHatObjects(new Uri("http://colorhat.com/apps/4/projects", UriKind.Absolute), 4, "project");
        }

        /// <summary>
        /// Gets the user's customers
        /// </summary>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        private async Task GetCustomers()
        {
            this.customers = await this.GetColorHatObjects(new Uri("http://colorhat.com/apps/7/customers", UriKind.Absolute), 7, "customer");
        }

        /// <summary>
        /// Gets the ColorHat objects from the specified URL
        /// </summary>
        /// <param name="address">
        /// The address.
        /// </param>
        /// <param name="appId">
        /// The app id.
        /// </param>
        /// <param name="appName">
        /// The app name.
        /// </param>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        private async Task<ReadOnlyDictionary<uint, string>> GetColorHatObjects(Uri address, ushort appId, string appName)
        {
            if (!this.IsLoggedIn)
            {
                throw new InvalidOperationException("You must be logged in to perform this operation.");
            }

            try
            {
                using (var handler = new HttpClientHandler { CookieContainer = this.cookies })
                using (var client = new HttpClient(handler))
                {
                    var getResult = await client.GetAsync(address);
                    getResult.EnsureSuccessStatusCode();

                    var content = await getResult.Content.ReadAsStringAsync();

                    var projectRegex = new Regex(string.Format("<a\\s+.*href=\"/apps/{0}/{1}/(?<id>[0-9]+)\"\\s+.*title=\"(?<title>.*)\"\\s+.*>", appId, appName));
                    return new ReadOnlyDictionary<uint, string>(projectRegex.Matches(content).Cast<Match>().ToDictionary(m => uint.Parse(m.Groups["id"].Value), m => m.Groups["title"].Value));
                }
            }
            catch (Exception ex)
            {
                throw new ColorhatException("Exception occured while attempting to get projects. See the inner exception for details.", ex);
            }
        }

        /// <summary>
        /// Dispatches an API request using the specified JSON data
        /// </summary>
        /// <param name="requestJson">
        /// The JSON request data.
        /// </param>
        /// <returns>
        /// True if successful, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        private async Task<bool> SendApiRequest(JSONItem requestJson)
        {
            if (!this.IsLoggedIn)
            {
                throw new InvalidOperationException("You must be logged in to perform this operation.");
            }

            try
            {
                using (var handler = new HttpClientHandler { CookieContainer = this.cookies })
                using (var client = new HttpClient(handler))
                {
                    var getResult = await client.PostAsync("http://colorhat.com/ajaxquery/RPC", new StringContent(requestJson.ToString()));
                    getResult.EnsureSuccessStatusCode();

                    var content = await getResult.Content.ReadAsStreamAsync();

                    var resultJson = JSONParser.ParseJSONStream(content, JSONParser.StreamEncoding.UTF8);

                    this.token = (string)resultJson["meta"]["token"].Value;
                    // ReSharper disable CompareOfFloatsByEqualityOperator
                    return (double)resultJson["meta"]["status"].Value == 200;
                    // ReSharper restore CompareOfFloatsByEqualityOperator
                }
            }
            catch (Exception ex)
            {
                throw new ColorhatException("Exception occured while attempting to upload time segment data. See the inner exception for details.", ex);
            }
        }

        /// <summary>
        /// The delete the specified ColorHat object.
        /// </summary>
        /// <param name="objectId">
        /// The service id.
        /// </param>
        /// <param name="tableName">
        /// The table name.
        /// </param>
        /// <returns>
        /// True on success, otherwise false.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Occurs when this methods executes before successful login attempt.
        /// </exception>
        /// <exception cref="ColorhatException">
        /// Occurs when there is an exception while communicating with the server.
        /// </exception>
        private async Task<bool> DeleteColorhatObject(uint objectId, string tableName)
        {
            var requestJson = 
                new JSONItem(
                    new KeyValuePair<string, JSONItem>(
                        "f", 
                        new JSONItem("addons_html_ListManager::deleteRow")), 
                    new KeyValuePair<string, JSONItem>(
                        "l", 
                        new JSONItem("http://colorhat.com/apps/6/services")),
                        new KeyValuePair<string, JSONItem>(
                            "p", 
                            new JSONItem(
                                new[]
                                    {
                                        new JSONItem(
                                            new KeyValuePair<string, JSONItem>("id", new JSONItem(tableName)), 
                                            new KeyValuePair<string, JSONItem>("listId", new JSONItem()),
                                            new KeyValuePair<string, JSONItem>("rowId", new JSONItem(objectId)))
                                    })),
                        new KeyValuePair<string, JSONItem>(
                            "t", 
                            new JSONItem(
                                new JSONItem(this.token), 
                                new JSONItem(this.tokenHash),
                                new JSONItem(this.snowflakeId))));

            return await this.SendApiRequest(requestJson);
        }
    }
}
